package cn.xnatural.app;

import cn.xnatural.app.util.Cron;
import cn.xnatural.enet.event.EL;

import java.time.Duration;
import java.util.Date;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

/**
 * 定时任务工具
 */
@Bean
public class Sched extends ServerTpl implements AutoCloseable {
    @Inject
    protected ScheduledExecutorService scheduler;
    protected final Queue<ScheduledFuture<?>> futures = new LinkedBlockingQueue<>();


    @EL(name = "sys.stopping")
    @Override
    public void close() {
        for (ScheduledFuture<?> f : futures) {
            f.cancel(true);
        }
    }


    /**
     * 添加 时间表达式 任务
     * @param cron 时间表达式 例: 0 0 1 * * ?
     * @param fn 要执行的函数
     * @return {@link Sched}
     */
    public Sched cron(String cron, Runnable fn) {
        return cron(cron, fn, null);
    }

    /**
     * 添加 时间表达式 任务
     * @param exp 时间表达式 例: 0 0 1 * * ?
     * @param fn 要执行的函数
     * @param stopCondition 停止条件
     * @return {@link Sched}
     */
    public Sched cron(String exp, Runnable fn, Supplier<Boolean> stopCondition) {
        if (exp == null || exp.isEmpty()) throw new IllegalArgumentException("Param exp not empty");
        if (fn == null) throw new IllegalArgumentException("Param fn required");
        Cron cron = new Cron(exp);
        return dynRate(
                () -> stopCondition == null || !stopCondition.get() ? cron.next() : null,
                Duration.ofMillis(cron.next().getTime() - new Date().getTime()),
                fn
        );
    }


    /**
     * 在将来的某个时间点执行
     * @param time 时间点
     * @param fn 任务函数
     * @return {@link ScheduledFuture}
     */
    public ScheduledFuture<?> time(Date time, Runnable fn) {
        if (time == null) throw new IllegalArgumentException("Param time required");
        if (fn == null) throw new IllegalArgumentException("Param fn required");
        return after(Duration.ofMillis(time.getTime() - System.currentTimeMillis()), fn);
    }


    /**
     * 在多少时间之后执行
     * @param duration 时间段
     * @param fn 任务函数
     * @return {@link ScheduledFuture}
     */
    @EL(name = "{name}.after")
    public ScheduledFuture<?> after(Duration duration, Runnable fn) {
        if (duration == null) throw new IllegalArgumentException("Param duration required");
        if (fn == null) throw new IllegalArgumentException("Param fn required");
        ScheduledFuture<?> future = scheduler.schedule(() -> async(fn), duration.toMillis(), TimeUnit.MILLISECONDS);
        futures.offer(future);
        futures.removeIf(f -> f.isDone() || f.isCancelled());
        return future;
    }

    /**
     * 任务间隔执行
     * {@link #fixedDelay}: 每次任务执行完成后才开始计算下次执行时间
     * {@link #fixedRate}: 每次执行即计算下次执行时间. 注意函数自己应该捕获异常
     * @param duration 间隔时间
     * @param initialDelay 第一次延迟多久后执行, 为空则立即执行
     * @param fn 任务函数
     * @return {@link Sched}
     */
    public Sched fixedDelay(Duration duration, Duration initialDelay, Runnable fn) {
        return fixedDelay(duration, initialDelay, fn, () -> false);
    }

    /**
     * 任务间隔执行
     * {@link #fixedDelay}: 每次任务执行完成后才开始计算下次执行时间
     * {@link #fixedRate}: 每次执行即计算下次执行时间. 注意函数自己应该捕获异常
     * @param duration 间隔时间
     * @param initialDelay 第一次延迟多久后执行, 为空则立即执行
     * @param fn 任务函数
     * @param stopCondition 停止条件
     * @return {@link Sched}
     */
    public Sched fixedDelay(Duration duration, Duration initialDelay, Runnable fn, Supplier<Boolean> stopCondition) {
        if (duration == null) throw new IllegalArgumentException("Param duration required");
        if (fn == null) throw new IllegalArgumentException("Param fn required");
        final Runnable wrapFn = new Runnable() {
            @Override
            public void run() {
                try {
                    fn.run();
                } finally {
                    if (stopCondition == null || !stopCondition.get()) {
                        after(duration, this);
                    }
                }
            }
        };
        if (initialDelay == null) wrapFn.run();
        else after(initialDelay, wrapFn);
        return this;
    }

    /**
     * 任务间隔执行
     * @param duration 间隔时间
     * @param fn 任务函数
     * @return {@link Sched}
     */
    public Sched fixedDelay(Duration duration, Runnable fn) {
        return fixedDelay(duration, null, fn);
    }

    /**
     * 时间间隔执行
     * {@link #fixedDelay}: 每次任务执行完成后才开始计算下次执行时间
     * {@link #fixedRate}: 每次执行即计算下次执行时间. 注意函数自己应该捕获异常
     * @param duration 间隔时间
     * @param initialDelay 第一次延迟多久后执行, 为空则立即执行
     * @param fn 任务函数
     * @param stopCondition 停止条件
     * @return {@link Sched}
     */
    public Sched fixedRate(Duration duration, Duration initialDelay, Runnable fn, Supplier<Boolean> stopCondition) {
        if (duration == null) throw new IllegalArgumentException("Param duration required");
        if (fn == null) throw new IllegalArgumentException("Param fn required");
        final Runnable wrapFn = new Runnable() {
            @Override
            public void run() {
                try {
                    async(fn);
                } finally {
                    if (stopCondition == null || !stopCondition.get()) {
                        after(duration, this);
                    }
                }
            }
        };
        if (initialDelay == null) wrapFn.run();
        else after(initialDelay, wrapFn);
        return this;
    }

    /**
     * 时间间隔执行
     * {@link #fixedDelay}: 每次任务执行完成后才开始计算下次执行时间
     * {@link #fixedRate}: 每次执行即计算下次执行时间. 注意函数自己应该捕获异常
     * @param duration 间隔时间
     * @param initialDelay 第一次延迟多久后执行, 为空则立即执行
     * @param fn 任务函数
     * @return {@link Sched}
     */
    public Sched fixedRate(Duration duration, Duration initialDelay, Runnable fn) {
        return fixedRate(duration, initialDelay, fn, () -> false);
    }

    /**
     * 时间间隔执行
     * @param duration 间隔时间
     * @param fn 任务函数
     * @return {@link Sched}
     */
    public Sched fixedRate(Duration duration, Runnable fn) {
        return fixedRate(duration, null, fn, () -> false);
    }


    /**
     * 动态任务调度执行. 自定义下次执行时间
     * @param dateSupplier 下次触发时间计算函数. 函数返回下次触发时间. 如果返回空 则停止
     * @param fn 任务函数
     * @return {@link Sched}
     */
    public Sched dynDelay(Supplier<Date> dateSupplier, Runnable fn) {
        return dynDelay(dateSupplier, null, fn);
    }

    /**
     * 动态任务调度执行. 自定义下次执行时间
     * @param dateSupplier 下次触发时间计算函数. 函数返回下次触发时间. 如果返回空 则停止
     * @param initialDelay 第一次延迟多久后执行, 为空则立即执行
     * @param fn 任务函数
     * @return {@link Sched}
     */
    public Sched dynDelay(Supplier<Date> dateSupplier, Duration initialDelay, Runnable fn) {
        if (dateSupplier == null) throw new IllegalArgumentException("Param dateSupplier required");
        if (fn == null) throw new IllegalArgumentException("Param fn required");
        Runnable wrapFn = new Runnable() {
            @Override
            public void run() {
                try {
                    fn.run();
                } finally {
                    Date d = dateSupplier.get();
                    if (d != null) time(d, this);
                }
            }
        };
        if (initialDelay == null) wrapFn.run();
        else after(initialDelay, wrapFn);
        return this;
    }

    /**
     * 动态任务调度执行. 自定义下次执行时间
     * @param dateSupplier 下次触发时间计算函数. 函数返回下次触发时间. 如果返回空 则停止
     * @param fn 任务函数
     * @return {@link Sched}
     */
    public Sched dynRate(Supplier<Date> dateSupplier, Runnable fn) {
        return dynRate(dateSupplier, null, fn);
    }

    /**
     * 动态任务调度执行. 自定义下次执行时间
     * @param dateSupplier 下次触发时间计算函数. 函数返回下次触发时间. 如果返回空 则停止
     * @param initialDelay 第一次延迟多久后执行, 为空则为空则立即执行
     * @param fn 任务函数
     * @return {@link Sched}
     */
    public Sched dynRate(Supplier<Date> dateSupplier, Duration initialDelay, Runnable fn) {
        if (dateSupplier == null) throw new IllegalArgumentException("Param dateSupplier required");
        if (fn == null) throw new IllegalArgumentException("Param fn required");
        Runnable wrapFn = new Runnable() {
            @Override
            public void run() {
                try {
                    async(fn);
                } finally {
                    Date d = dateSupplier.get();
                    if (d != null) time(d, this);
                }
            }
        };
        if (initialDelay == null) wrapFn.run();
        else after(initialDelay, wrapFn);
        return this;
    }
}
